Merged [21375] and [21376] - fixes to IKImagePickerForTiger [21372]
[adiumx.git] / Frameworks / AIUtilities Framework / Source / AIImageViewWithImagePicker.m
blob94a95238905e80e88776ba3ca8f20bda7736108a
1 //
2 //  AIImageViewWithImagePicker.m
3 //  Adium
4 //
5 //  Created by Evan Schoenberg on Sun Jun 06 2004.
6 //  Copyright (c) 2004-2005 The Adium Team. All rights reserved.
7 //
9 #import "AITigerCompatibility.h"
11 #import "AIImageViewWithImagePicker.h"
12 #import <Quartz/Quartz.h>
14 #import "AIImageAdditions.h"
15 #import "AIFileManagerAdditions.h"
16 #import "AIApplicationAdditions.h"
17 #import "AIStringUtilities.h"
18 #import "IKPictureTakerForTiger.h"
20 #define DRAGGING_THRESHOLD 16.0
22 @interface AIImageViewWithImagePicker (PRIVATE)
23 - (void)_initImageViewWithImagePicker;
24 - (void)showPictureTaker;
25 - (void)copy:(id)sender;
26 - (void)paste:(id)sender;
27 - (void)delete;
28 @end
31 #define IKPictureTakerClass ([NSApp isOnLeopardOrBetter] ? NSClassFromString(@"IKPictureTaker") : NSClassFromString(@"IKPictureTakerForTiger"))
34  * @class AIImageViewWithImagePicker
35  *
36  * @brief Image view which displays and uses the Image Picker used by Apple Address Book and iChat when activated and also allows other image-setting behaviors.
37  *
38  * The following is supported
39  *              - Address book-style image picker on double-click or enter, with delegate notification
40  *              - Or, alternately, an Open Panel on double-click or enter, with delegate notification
41  *              - Copying and pasting, with delegate notification
42  *              - Drag and drop into and out of the image well, with delegate notification, 
43  *                      with support for animated GIFs and transparency
44  *              - Notifcation to the delegate of user's attempt to delete the image
45  *
46  * Note: AIImageViewWithImagePicker requires Panther or better for the Address Book-style
47  * image picker to work.
48  */
49 @implementation AIImageViewWithImagePicker
51 // Init ------------------------------------------------------------------------------------------
52 #pragma mark Init
54  * @brief Initialize with coder
55  */
56 - (id)initWithCoder:(NSCoder *)aDecoder
58     if ((self = [super initWithCoder:aDecoder])) {
59                 [self _initImageViewWithImagePicker];
60         }
61     return self;
65  * @brief Initialize with frame
66  */
67 - (id)initWithFrame:(NSRect)frameRect
69         if ((self = [super initWithFrame:frameRect])) {
70                 [self _initImageViewWithImagePicker];
71         }
72         return self;
76  * @brief Private initialization method
77  */
78 - (void)_initImageViewWithImagePicker
80         pictureTaker = nil;
81         title = nil;
82         delegate = nil;
83         
84         lastResp = nil;
85         shouldDrawFocusRing = NO;
87         mouseDownPos = NSZeroPoint;
88         maxSize = NSZeroSize;
90         usePictureTaker = YES;
94  * @brief Deallocate
95  */
96 - (void)dealloc
98         if (pictureTaker) {
99                 [pictureTaker close];
100                 [pictureTaker release]; pictureTaker = nil;
101         }
102         
103         delegate = nil;
104         [title release];
105         
106         [super dealloc];
109 // Getters and Setters ----------------------------------------------------------------
110 #pragma mark Getters and Setters
112  * @brief Set the delegate
114  * Set the delegate.  See <tt>AIImageViewWithImagePickerDelegate</tt> protocol discussion for details.
115  * @param inDelegate The delegate, which may implement any of the methods described in <tt>AIImageViewWithImagePickerDelegate</tt>.
116  */ 
117 - (void)setDelegate:(id)inDelegate
119         delegate = inDelegate;
123  * @brief Return the delegate
125  * @return The delegate
126  */ 
127 - (id)delegate
129         return delegate;
133  * @brief Set the image
135  * We may get here progrmatically, from a user drag-and-drop or paste, etc.
136  */
137 - (void)setImage:(NSImage *)inImage
139         [super setImage:inImage];
140         
141         //Inform the picker controller of a changed selection if it is open, for live updating
142         if (pictureTaker) {
143                 [pictureTaker setInputImage:inImage];
144         }
148  * @brief Set the title of the Image Picker
150  * Set the title of the Image Picker window which will be displayed if the user activates it (see class discussion).
151  * @param inTitle An <tt>NSString</tt> of the title
152  */ 
153 - (void)setTitle:(NSString *)inTitle
155         if (title != inTitle) {
156                 [title release]; title = [inTitle retain];
157                 if (pictureTaker) {
158                         [pictureTaker setTitle:title];
159                 }
160         }
164  * @brief The title of the image picker
165  */
166 - (NSString *)title
168         return (title ? title : AILocalizedStringFromTableInBundle(@"Image Picker", nil, [NSBundle bundleWithIdentifier:AIUTILITIES_BUNDLE_ID], nil));
172  * @brief Should the image view use the address book Image Picker?
174  * If NO, a standard Open panel is used instead.
175  */
176 - (void)setUsePictureTaker:(BOOL)inUsePictureTaker
178         usePictureTaker = inUsePictureTaker;
181 - (void)setPresentPictureTakerAsSheet:(BOOL)inPresentPictureTakerAsSheet
183         presentPictureTakerAsSheet  = inPresentPictureTakerAsSheet;
186 - (BOOL)presentPictureTakerAsSheet
188         return presentPictureTakerAsSheet;
191 - (void)setMaxSize:(NSSize)inMaxSize
193         maxSize = inMaxSize;
196 - (NSSize)maxSize
198         return maxSize;
201 // Monitoring user interaction --------------------------------------------------------
202 #pragma mark Monitoring user interaction
205  * @brief Mouse down
207  * Intercept mouse down events so we can begin a drag out of the image view if appropriate
208  */
209 - (void)mouseDown:(NSEvent *)theEvent
211         if ([self isEnabled]) {
212                 NSEvent *nextEvent;
213                 
214                 //Wait for the next event
215                 nextEvent = [[self window] nextEventMatchingMask:(NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSPeriodicMask)
216                                                                                            untilDate:[NSDate distantFuture]
217                                                                                                   inMode:NSEventTrackingRunLoopMode
218                                                                                                  dequeue:NO];
219                 
220                 mouseDownPos = [self convertPoint:[theEvent locationInWindow] fromView:nil];
221                 
222                 /* If the user starts dragging, don't call mouse down as we won't receive mouse dragged events, as it seems that
223                         * NSImageView does some sort of event loop modification in response to a click. We didn't dequeue the event, so
224                         * we don't have to handle it ourselves -- instead, the event loop will handle it after this invocation is complete. 
225                         */
226                 if ([nextEvent type] != NSLeftMouseDragged) {
227                         [super mouseDown:theEvent];   
228                 }
229                 
230                 if ([theEvent clickCount] == 2) {
231                         [self showPictureTaker];
232                 }
234         } else {
235                 [super mouseDown:theEvent];   
236         }
240  * @brief Key down
242  * Intercept key down events to delete the image on delete/backspace or to show the image picker on enter/return
243  */
244 - (void)keyDown:(NSEvent *)theEvent
246         NSString *characters = [theEvent charactersIgnoringModifiers];
247         unichar key = ([characters length] ? [characters characterAtIndex:0] : 0);
248         
249         if ((key == NSBackspaceCharacter) || (key == NSDeleteCharacter) || (key == NSDeleteFunctionKey) || (key == NSDeleteCharFunctionKey)) {
250                 [self delete];
251         } else if (key == NSEnterCharacter || key == NSCarriageReturnCharacter) {
252                 [self showPictureTaker];
253         } else {
254                 [super keyDown:theEvent];
255         }
259  * @brief Mouse dragged
261  * Begin an image drag as appropriate
262  */
263 - (void)mouseDragged:(NSEvent *)theEvent
265         if (![self image]) return;
267         // Work out if the mouse has been dragged far enough - it stops accidental drags
268         NSPoint mousePos = [self convertPoint:[theEvent locationInWindow] fromView:nil];
269         float dx = mousePos.x-mouseDownPos.x;
270         float dy = mousePos.y-mouseDownPos.y;   
271         if ((dx*dx) + (dy*dy) < DRAGGING_THRESHOLD) {
272                 return;
273         }
274         
275         //Start the drag
276         [self dragPromisedFilesOfTypes:[NSArray arrayWithObject:@"png"]
277                                                   fromRect:NSZeroRect
278                                                         source:self
279                                                  slideBack:YES
280                                                          event:theEvent];
283 - (void)dragImage:(NSImage *)anImage at:(NSPoint)imageLoc offset:(NSSize)mouseOffset event:(NSEvent *)theEvent pasteboard:(NSPasteboard *)pboard source:(id)sourceObject slideBack:(BOOL)slideBack
285         [pboard addTypes:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,nil] owner:self];
286         
287         NSImage *dragImage = [[NSImage alloc] initWithSize:[[self image] size]];
288         
289         //Draw our original image as 50% transparent
290         [dragImage lockFocus];
291         [[self image] dissolveToPoint:NSZeroPoint fraction:0.5];
292         [dragImage unlockFocus];
293         
294         //We want the image to resize
295         [dragImage setScalesWhenResized:YES];
296         //Change to the size we are displaying
297         [dragImage setSize:[self bounds].size];
298         
299         [super dragImage:dragImage
300                                   at:imageLoc
301                           offset:mouseOffset
302                            event:theEvent
303                   pasteboard:pboard
304                           source:sourceObject
305                    slideBack:slideBack];
306         [dragImage release];
310  * @brief Declare what operations we can participate in as a drag and drop source
311  */
312 - (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)flag
314         return NSDragOperationCopy;
318  * @brief Method called to support drag types we said we could offer
319  */
320 - (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type
322     //sender has accepted the drag and now we need to send the data for the type we promised
323     if ([type isEqualToString:NSTIFFPboardType]) {
324                 //set data for TIFF type on the pasteboard as requested
325                 [sender setData:[[self image] TIFFRepresentation] 
326                                 forType:NSTIFFPboardType];
327                 
328     } else if ([type isEqualToString:NSPDFPboardType]) {
329                 [sender setData:[self dataWithPDFInsideRect:[self bounds]] 
330                                 forType:NSPDFPboardType];
331     }
335  * @brief Dragging entered
336  */
337 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
339         if ([sender draggingSource] == self) {
340                 return NSDragOperationNone;
341         } else {
342                 return [super draggingEntered:sender];
343         }
347  * @brief Dragging updated
348  */
349 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
351         if ([sender draggingSource] == self) {
352                 return NSDragOperationNone;
353         } else {
354                 return [super draggingUpdated:sender];
355         }
358 - (NSArray *)namesOfPromisedFilesDroppedAtDestination:(NSURL *)inDropDestination
360         NSString *name = nil;
361         if ([[self delegate] respondsToSelector:@selector(fileNameForImageInImagePicker:)]) {
362                 name = [[self delegate] fileNameForImageInImagePicker:self];
363                 if (![name length]) name = nil;
364         }
365         
366         if (!name)
367                 name = NSLocalizedString(@"Picture", nil);
368         
369         name = [name stringByAppendingPathExtension:@"png"];
370         
371         NSString *fullPath = [[inDropDestination path] stringByAppendingPathComponent:name];
372         fullPath = [[NSFileManager defaultManager] uniquePathForPath:fullPath];
373         
374         [[[self image] PNGRepresentation] writeToFile:fullPath
375                                                                            atomically:YES];
376         
377         return [NSArray arrayWithObject:[fullPath lastPathComponent]];
381  * @brief Conclude a drag operation
383  * A new image was dragged into our view.  -[super concludeDragOperation:] will change [self image] to match it.
384  * We then want to update our pictureTaker's selection if it is open.
385  * Also, if we're dropped a promised file, use its data directly as it may be better than what NSImageView's natural
386  * loading retrieves... this way we can get transparency or animation data, for example.
387  */
388 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
390         BOOL    notified = NO, resized = NO;
391         NSImage *droppedImage;
392         NSSize  droppedImageSize;
394         [super concludeDragOperation:sender];
396         droppedImage = [self image];
397         droppedImageSize = [droppedImage size];
399         if ((maxSize.width > 0 && droppedImageSize.width > maxSize.width) ||
400                 (maxSize.height > 0 && droppedImageSize.height > maxSize.height)) {
401                 droppedImage = [droppedImage imageByScalingToSize:maxSize];
402                 //This will notify the picker controller that the selection changed, as well
403                 [self setImage:droppedImage];
404                 resized = YES;
406         } else if (pictureTaker) {
407                 [pictureTaker setInputImage:droppedImage];
408         }
410         //Use the file's data if possible and the image wasn't too big
411         if (!resized && [delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
412                 NSPasteboard    *pboard = [sender draggingPasteboard];
414                 if ([[pboard types] containsObject:NSFilenamesPboardType]) {
415                         NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
416                 
417                         if ([files count]) {
418                                 NSString        *imageFile = [files objectAtIndex:0];
419                                 NSData          *imageData = [NSData dataWithContentsOfFile:imageFile];
421                                 if (imageData) {
422                                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
423                                                                    withObject:self
424                                                                    withObject:[NSData dataWithContentsOfFile:imageFile]];
425                                         
426                                         notified = YES;
427                                 }
428                         }
429                 }
430         }
432         //Inform the delegate if we haven't informed it yet
433         if (!notified) {
434                 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
435                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
436                                                    withObject:self
437                                                    withObject:[droppedImage PNGRepresentation]];
439                 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
440                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
441                                                    withObject:self
442                                                    withObject:droppedImage];
443                 }
444         }
447 // Copy / Paste ----------------------------------------------------------------
448 #pragma mark Copy / Paste
450  * @brief Copy
451  */
452 - (void)copy:(id)sender
454         NSImage *image = [self image];
455         if (image) {
456                 [[NSPasteboard generalPasteboard] declareTypes:[NSArray arrayWithObject:NSTIFFPboardType] owner:nil];
457                 [[NSPasteboard generalPasteboard] setData:[image TIFFRepresentation] forType:NSTIFFPboardType];
458         }
462  * @brief Paste
463  */
464 - (void)paste:(id)sender
466         NSPasteboard    *pb = [NSPasteboard generalPasteboard];
467         NSString                *type = [pb availableTypeFromArray:
468                 [NSArray arrayWithObjects:NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType,nil]];
469         BOOL                    success = NO;
471     NSData                      *imageData = (type ? [pb dataForType:type] : nil);
472         if (imageData) {
473                 NSImage *image = [[[NSImage alloc] initWithData:imageData] autorelease];
474                 if (image) {
475                         NSSize  imageSize = [image size];
477                         if ((maxSize.width > 0 && imageSize.width > maxSize.width) ||
478                                 (maxSize.height > 0 && imageSize.height > maxSize.height)) {
479                                 image = [image imageByScalingToSize:maxSize];
480                                 imageData = [image PNGRepresentation];
481                         }
482                         
483                         [self setImage:image];
484                                                         
485                         if (pictureTaker) {
486                                 [pictureTaker setInputImage:image];
487                         }
488                         
489                         //Inform the delegate
490                         if (delegate) {
491                                 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
492                                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
493                                                                    withObject:self
494                                                                    withObject:imageData];
495                                 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
496                                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
497                                                                    withObject:self
498                                                                    withObject:image];
499                                 }
500                         }
501                         
502                         success = YES;
503                 }
504         }
505         
506         if (!success) NSBeep();
510  * @brief Cut
512  * Cut = copy + delete
513  */
514 - (void)cut:(id)sender
516         [self copy:sender];
517         [self delete];
521  * @brief Delete
522  */
523 - (void)delete
525         if (delegate && [delegate respondsToSelector:@selector(deleteInImageViewWithImagePicker:)]) {
526                 [delegate performSelector:@selector(deleteInImageViewWithImagePicker:)
527                                            withObject:self];
528         }       
531 // NSImagePicker Access and Delegate ----------------------------------------------------------------
532 #pragma mark NSImagePicker Access and Delegate
534  * @brief Action to call -[self showPictureTaker]
535  */ 
536 - (IBAction)showImagePicker:(id)sender
538         [self showPictureTaker];
541 - (void)pictureTakerDidEnd:(id)inPictureTaker returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
542 {       
543         if (returnCode == NSOKButton) {
544                 NSImage *image = [inPictureTaker outputImage];
545                 
546                 //Update the NSImageView
547                 [self setImage:image];
548                 
549                 //Inform the delegate, but only if NOT using NSOpenPanel
550                 if (delegate && usePictureTaker) {
551                         if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
552                                 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
553                                                            withObject:self
554                                                            withObject:[image PNGRepresentation]];
555                                 
556                         } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
557                                 [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
558                                                            withObject:self
559                                                            withObject:image];
560                         }
561                 }
562         }
566  * @brief Show the image picker controller
567  */
568 - (void)showPictureTaker
570         if (usePictureTaker) {
571                 if (!pictureTaker) {    
572                         pictureTaker = [[IKPictureTakerClass pictureTaker] retain];
573                         [pictureTaker setDelegate:self];
574                         [pictureTaker setTitle:title];
575                 }
576                          
577                 NSImage *theImage = nil;
578                          
579                 //Give the delegate an opportunity to supply an image which differs from the NSImageView's image
580                 if (delegate && [delegate respondsToSelector:@selector(imageForImageViewWithImagePicker:)]) {
581                         theImage = [delegate imageForImageViewWithImagePicker:self];
582                 }
583                 
584                 [pictureTaker setInputImage:(theImage ? theImage : [self image])];
585                 [pictureTaker setValue:[NSValue valueWithSize:[self maxSize]]
586                                                 forKey:IKPictureTakerOutputImageMaxSizeKey];
588                 if ([self presentPictureTakerAsSheet]) {
589                         [pictureTaker beginPictureTakerSheetForWindow:[self window] 
590                                                                                          withDelegate:self
591                                                                                    didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
592                                                                                           contextInfo:nil];
593                 } else {
594                         [pictureTaker beginPictureTakerWithDelegate:self
595                                                                                  didEndSelector:@selector(pictureTakerDidEnd:returnCode:contextInfo:)
596                                                                                         contextInfo:nil];
597                 }
598                          
599         } else {
600                 /* If we aren't using or can't use the image picker, use an open panel  */
601                 NSOpenPanel *openPanel;
602                 
603                 openPanel = [NSOpenPanel openPanel];
604                 [openPanel setTitle:[NSString stringWithFormat:AILocalizedStringFromTableInBundle(@"Select Image", nil, [NSBundle bundleWithIdentifier:AIUTILITIES_BUNDLE_ID], nil)]];
605                 
606                 if ([openPanel runModalForDirectory:nil file:nil types:[NSImage imageFileTypes]] == NSOKButton) {
607                         NSData  *imageData;
608                         NSImage *image;
609                         NSSize  imageSize;
611                         imageData = [NSData dataWithContentsOfFile:[openPanel filename]];
612                         image = (imageData ? [[[NSImage alloc] initWithData:imageData] autorelease] : nil);
613                         imageSize = (image ? [image size] : NSZeroSize);
615                         if ((maxSize.width > 0 && imageSize.width > maxSize.width) ||
616                                 (maxSize.height > 0 && imageSize.height > maxSize.height)) {
617                                 image = [image imageByScalingToSize:maxSize];
618                                 imageData = [image PNGRepresentation];
619                         }
620                         
621                         //Update the image view
622                         [self setImage:image];
623                         
624                         //Inform the delegate
625                         if (delegate) {
626                                 if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)]) {
627                                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImageData:)
628                                                                    withObject:self
629                                                                    withObject:imageData];
630                                         
631                                 } else if ([delegate respondsToSelector:@selector(imageViewWithImagePicker:didChangeToImage:)]) {
632                                         [delegate performSelector:@selector(imageViewWithImagePicker:didChangeToImage:)
633                                                                    withObject:self
634                                                                    withObject:image];
635                                 }
636                         }
637                 }
638         }
640                                                                                                                                                                   
641 // Drawing ------------------------------------------------------------------------
642 #pragma mark Drawing
644  * @brief Note when the focus ring needs to be displayed
646  * Focus ring drawing code by Nicholas Riley, posted unlicensed as public domain on cocoadev and available at:
647  * http://cocoa.mamasam.com/COCOADEV/2002/03/2/29535.php
648  */
649 - (BOOL)needsDisplay
651         NSResponder *resp = nil;
652         NSWindow        *window = [self window];
653         
654         if ([window isKeyWindow]) {
655                 resp = [window firstResponder];
656                 if (resp == lastResp) {
657                         return [super needsDisplay];
658                 }
659                 
660         } else if (lastResp == nil) {
661                 return [super needsDisplay];
662                 
663         }
664         
665         shouldDrawFocusRing = (resp != nil &&
666                                                    [resp isKindOfClass:[NSView class]] &&
667                                                    [(NSView *)resp isDescendantOf:self]); // [sic]
668         lastResp = resp;
669         
670         [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]];
671         return YES;
675  * @brief Draw the focus ring around our view if necessary
676  */
677 - (void)drawRect:(NSRect)rect
679         [super drawRect:rect];
680         
681         if (shouldDrawFocusRing) {
682                 NSSetFocusRingStyle(NSFocusRingOnly);
683                 NSRectFill(rect);
684         }
687 @end